package com.zrj.gulimall.product.service.impl;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.TypeReference;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.zrj.common.utils.PageUtils;
import com.zrj.common.utils.Query;
import com.zrj.gulimall.product.dao.CategoryDao;
import com.zrj.gulimall.product.entity.CategoryEntity;
import com.zrj.gulimall.product.service.CategoryBrandRelationService;
import com.zrj.gulimall.product.service.CategoryService;
import com.zrj.gulimall.product.vo.Catelog2Vo;
import org.apache.commons.lang.StringUtils;
import org.redisson.api.RLock;
import org.redisson.api.RReadWriteLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;


@Service("categoryService")
public class CategoryServiceImpl extends ServiceImpl<CategoryDao, CategoryEntity> implements CategoryService {

    @Autowired
    private CategoryBrandRelationService categoryBrandRelationService;

    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    @Autowired
    private RedissonClient redissonClient;

    @Override
    public PageUtils queryPage(Map<String, Object> params) {
        IPage<CategoryEntity> page = this.page(
                new Query<CategoryEntity>().getPage(params),
                new QueryWrapper<CategoryEntity>()
        );

        return new PageUtils(page);
    }

    @Override
    public List<CategoryEntity> buildTree() {
        //查出所有的分类
        List<CategoryEntity> entities = baseMapper.selectList(null);
        //组装成树形结构
        return entities.stream()
                //filter是找不到符合的就不会往下执行了
                .filter(entity ->
                //找到所有一级分类
                entity.getParentCid() == 0)
                //peek和map的区别在于peek是没有返回值的而map是有返回值的
                .peek((menu) ->
                        //找到子菜单
                        menu.setChildren(getChildren(menu,entities))).collect(Collectors.toList());
    }

    @Override
    public List<Long> findCatelogPath(Long catelogId) {
        List<Long> paths = new ArrayList<>();
        //递归查询是否还有父节点
        List<Long> parentPath = findParentPath(catelogId, paths);
        //反转数组
        Collections.reverse(parentPath);
        return parentPath;
    }

    /**
     * 级联更新所有关联的数据
     * @CacheEvict：失效模式
     * @CachePut：双写模式，须要有返回值
     * 1、同时进行多种缓存操作： @Caching
     * 2、指定删除某一个分区下的所有数据@CacheEvict(value = {"category"}, allEntries = true)
     * 3、存储同一类型的数据，都可以指定为同一分区
     * @param category
     */
    // @Caching(evict = {
    //         @CacheEvict(value = "category",key = "'getLevel1Categorys'"),
    //         @CacheEvict(value = "category",key = "'getCatalogJson'")
    // })
    @CacheEvict(value = {"category"}, allEntries = true)
    @Transactional
    @Override
    public void updateCategory(CategoryEntity category) {
        updateById(category);
        if(StringUtils.isNotEmpty(category.getName())) {
            categoryBrandRelationService.updateCategoryName(category.getCatId(), category.getName());
            //TODO 更新其他关联
        }
    }

    /**
     * 每一个需要缓存的数据我们都来指定要放到哪个名字的缓存。【缓存的分区（按照业务类型分）】
     * 代表当前方法的结果需要缓存，如果缓存中有，方法都不用调用，如果缓存中没有，会调用方法。最后将方法的的结果放入缓存
     * 默认行为：
     *        如果缓存中有，方法不再调用
     *        key是默认生成的：缓存的名字::SimpleKey::[]（自动会生成key值）
     *        缓存的value值，默认使用jdk序列化机制，将序列化的书籍存到redis中
     *        默认时间是-1
     * 自定义操作：key的生成
     *        指定生成缓存的key：key属性指定，接受一个Spel表达式
     *        指定缓存的数据存活时间：配置文档中修改存活时间
     *        将数据保存为json格式
     *
     *  4、spring-cache的不足之处
     *      1、读模式
     *          缓存穿透：查询一个null数据。解决方法：缓存空数据
     *          缓存击穿：大量并发进来同时查询一个正好过去的数据。解决方案：加锁？默认是无锁的；使用sync=true来解决击穿问题
     *          缓存雪崩：大量的key同时过期。解决：加随机时间。加上过期时间
     *      2、写模式：（缓存与数据库一致）
     *          1、加读写锁
     *          2、引用canal，感知到mysql的更新去更新缓存
     *          3、读多写多，直接去数据库查询就行
     *
     *  总结：
     *      常规数据（读多写少，即时性，一致性要求不高的数据，完全可以使用spring-cache：写模式（只要缓存的数据有过期时间就足够了）
     *      特俗数据：特殊设计
     * @return
     */
    @Cacheable(value = {"category"}, key = "#root.methodName", sync = true)
    @Override
    public List<CategoryEntity> getLevel1Categorys() {
        LambdaQueryWrapper<CategoryEntity> wrapper = new LambdaQueryWrapper<>();
        wrapper.eq(CategoryEntity::getParentCid, 0);
        return list(wrapper);
    }


    @Cacheable(value = {"category"}, key = "#root.methodName", sync = true)
    @Override
    public Map<String, List<Catelog2Vo>> getCatalogJson(){
        System.out.println("查询数据库");
        //将所有的分类数据查出
        List<CategoryEntity> categoryEntityList = this.list();
        //获取一级分类
        List<CategoryEntity> level1Categorys = getParentCategory(categoryEntityList, 0L);
        //封装数据
        return level1Categorys.stream().collect(Collectors.toMap(k -> k.getCatId().toString(), v -> {
            //1、每一个的一级分类,查到这个一级分类的二级分类
            List<CategoryEntity> level2Categorys = getParentCategory(categoryEntityList, v.getCatId());
            //2、封装上面的结果
            List<Catelog2Vo> catelog2Vos = new ArrayList<>();
            if (level2Categorys != null) {
                catelog2Vos = level2Categorys.stream().map(l2 -> {
                    Catelog2Vo catelog2Vo = new Catelog2Vo(v.getCatId().toString(), null, l2.getCatId().toString(), l2.getName());
                    //1、找当前二级分类的三级分类封装成vo
                    List<CategoryEntity> level3Catelog = getParentCategory(categoryEntityList, l2.getCatId());
                    if (level3Catelog != null) {
                        List<Catelog2Vo.Category3Vo> category3Vos = level3Catelog.stream().map(l3 -> {
                            //2、封装成指定格式
                            return new Catelog2Vo.Category3Vo(l2.getCatId().toString(), l3.getCatId().toString(), l3.getName());
                        }).collect(Collectors.toList());
                        catelog2Vo.setCatalog3List(category3Vos);
                    }
                    return catelog2Vo;
                }).collect(Collectors.toList());
            }
            return catelog2Vos;
        }));
    }


    public Map<String, List<Catelog2Vo>> getCatalogJson2() {
        //从redis中判断数据是否在缓存中
        ValueOperations<String, String> ops = stringRedisTemplate.opsForValue();
        String catalogJson = ops.get("catalogJson");
        if(StringUtils.isEmpty(catalogJson)) {
            //没有就查询数据库，并把数据放到缓存中
            Map<String, List<Catelog2Vo>> catalogJsonFormDb = getCatalogJsonFromDbWithRedissonLock();
            String s = JSON.toJSONString(catalogJsonFormDb);
            ops.set("catalogJson", s);
        }
        return JSON.parseObject(catalogJson, new TypeReference<Map<String, List<Catelog2Vo>>>() {
        });
    }

    public Map<String, List<Catelog2Vo>> getCatalogJsonFromDbWithRedissonLock() {
        //1、占分布式锁。去redis占坑
        //（锁的粒度，越细越快:具体缓存的是某个数据，11号商品） product-11-lock
        //RLock catalogJsonLock = redissonClient.getLock("catalogJson-lock");
        //创建读锁
        RReadWriteLock readWriteLock = redissonClient.getReadWriteLock("catalogJson-lock");
        RLock lock = readWriteLock.readLock();
        Map<String, List<Catelog2Vo>> dataFromDb = null;
        try {
            lock.lock();
            //加锁成功...执行业务
            dataFromDb = getCatalogJsonFormDb();
        }finally {
            lock.unlock();
        }
        return dataFromDb;
    }

    public Map<String, List<Catelog2Vo>> getCatalogJsonFormDb(){
        //得到锁以后，我们应该再去缓存中确定一次，如果没有才需要继续查询
        String catalogJson = stringRedisTemplate.opsForValue().get("catalogJson");
        if (!org.springframework.util.StringUtils.isEmpty(catalogJson)) {
            //缓存不为空直接返回
            return JSON.parseObject(catalogJson, new TypeReference<Map<String, List<Catelog2Vo>>>() {
            });
        }

        //将所有的分类数据查出
        List<CategoryEntity> categoryEntityList = this.list();
        //获取一级分类
        List<CategoryEntity> level1Categorys = getParentCategory(categoryEntityList, 0L);
        //封装数据
        return level1Categorys.stream().collect(Collectors.toMap(k -> k.getCatId().toString(), v -> {
            //1、每一个的一级分类,查到这个一级分类的二级分类
            List<CategoryEntity> level2Categorys = getParentCategory(categoryEntityList, v.getCatId());
            //2、封装上面的结果
            List<Catelog2Vo> catelog2Vos = null;
            if (level2Categorys != null) {
                catelog2Vos = level2Categorys.stream().map(l2 -> {
                    Catelog2Vo catelog2Vo = new Catelog2Vo(v.getCatId().toString(), null, l2.getCatId().toString(), l2.getName());
                    //1、找当前二级分类的三级分类封装成vo
                    List<CategoryEntity> level3Catelog = getParentCategory(categoryEntityList, l2.getCatId());
                    if (level3Catelog != null) {
                        List<Catelog2Vo.Category3Vo> category3Vos = level3Catelog.stream().map(l3 -> {
                            //2、封装成指定格式
                            return new Catelog2Vo.Category3Vo(l2.getCatId().toString(), l3.getCatId().toString(), l3.getName());
                        }).collect(Collectors.toList());
                        catelog2Vo.setCatalog3List(category3Vos);
                    }
                    return catelog2Vo;
                }).collect(Collectors.toList());
            }
            return catelog2Vos;
        }));
    }

    /**
     * 加本地锁，但在分布式的情况下会出现问题
     * @return
     */
    public Map<String, List<Catelog2Vo>> getCatalogJsonFromDbWithLocalLock(){
        synchronized (this) {
            return this.getCatalogJsonFormDb();
        }
    }

    private List<CategoryEntity> getParentCategory(List<CategoryEntity> categoryEntityList, Long parentCid){
        return categoryEntityList.stream().filter(item -> item.getParentCid().equals(parentCid)).collect(Collectors.toList());
    }

    /**
     * 递归查询
     * @param catelogId
     * @param paths
     * @return
     */
    public List<Long> findParentPath(Long catelogId, List<Long> paths){
        paths.add(catelogId);
        CategoryEntity entity = getById(catelogId);
        //判断是不是父分类，如果不是就继续递归进行
        if(entity.getParentCid() != 0){
            findParentPath(entity.getParentCid(), paths);
        }
        return paths;
    }

    private List<CategoryEntity> getChildren(CategoryEntity menu, List<CategoryEntity> entities) {
        return entities.stream().filter(e -> menu.getCatId().equals(e.getParentCid()))
                .peek(menu1 -> menu1.setChildren(getChildren(menu1, entities))).collect(Collectors.toList());
    }

}